#####################################################################
# Product:      KnowledgeEngine (Plone Edition)
#
# File:         Selection.py
#
# Copyright:    Copyright (c) 2004, Faulkner Technologies
#
# Author:       Brian Oliver
#
# Description:  A Selection is a primitive Component
#               to represent multiple simple true/false Options
#               in an assessment.  It is a container for Options (only)
#               and permits both single and multilpe selections of options
#               with a variety of display formats.  Unlike the 
#               MultipleChoice component, it does not support the
#               concept of correctness.
#
# See Also:     Component, Model, Option
#####################################################################

#####################################################################
## Knowledge Engine Library Imports
from Products.KnowledgeEngine.Common import *
from Products.KnowledgeEngine.Core import *
from Products.KnowledgeEngine.Component import Component, ComponentInfo
from Products.KnowledgeEngine.AssessmentFilter import AssessmentFilter
from Products.KnowledgeEngine.AssessmentComponentState import AssessmentComponentState
from Products.KnowledgeEngine.AssessmentComponentValueTypes import *
from Products.KnowledgeEngine.Exportable import Exportable
from Products.KnowledgeEngine.KERLProcessor import *
from Products.KnowledgeEngine.PropertyDefinition import *
from Products.KnowledgeEngine.PropertyTypes import *
from Products.KnowledgeEngine.PropertyFormats import *
from Products.KnowledgeEngine.PropertyStructures import *
from Products.KnowledgeEngine.Graphing.Graphable import *
from Products.KnowledgeEngine.Graphing.Datum import *
from Products.KnowledgeEngine.User import User 
from Products.KnowledgeEngine.Caching import RequestCache 

#####################################################################
## Zope CMF Library Imports
from Products.CMFCore.PortalContent import PortalContent
from Products.CMFCore.ActionInformation import ActionInformation
from Products.CMFCore import permissions
from Products.CMFCore.permissions import ModifyPortalContent

#####################################################################
## Zope Library Imports
from Globals import DTMLFile
from App.ImageFile import ImageFile

#####################################################################
## Python Library Imports
# (none)

DISPLAY_FORMAT_OPTIONS = ['Radio Buttons','Check Boxes','Dropdown List','Multi Select List']

#####################################################################
## SelectionInfo Class Definition
class SelectionInfo(ComponentInfo):
    """
        Defines the class info methods, defining information about
        the class without needing an instance of the class
    """

    #---------------------------------------------------------------#

    def getClassName(self):
        """
            Return the class name
        """
        return 'Selection'

    #---------------------------------------------------------------#

    def getDisplayName(self):
        """
            Return a friendly name for the class instances
            (this is used for displaying in lists etc)
        """
    
        return "Selection Question"
        
    #---------------------------------------------------------------#

    def getIdentityType(self):
        """
            Return the identity type.  
        """
        
        return "sel"
        
    #---------------------------------------------------------------#
    
    def getClassIcon(self):
        """
            Return the appropriate icon file for the class.
        """
        
        return ImageFile('skins/Selection_icon.gif', globals())

    #---------------------------------------------------------------#

    def isInstanceOf(self, className):
        """
            Returns if the class is an instance of the specified
            className, or implements the interface specified 
            by the className
        """

        return className == 'Exportable' or ComponentInfo.isInstanceOf(self, className)

    #---------------------------------------------------------------#

    def isContainer(self):
        """
            Returns if the instance is providing folderish/container
            services.
            
            Override an return true if the instance is to be
            a container for other instances.
            
            NOTE: all instances support "events" so there is no
            need to return true if you require "event" support.
        """

        return true          

    #---------------------------------------------------------------#

    def isChildPermitted(self, childClassInfo, childInstance = None):
        """
            Returns if the specified child is permitted as a 
            child of the class' instances.
            
            NOTE: This is from this class' instance perspective.
            
            For component containers, this always overrides what the
            return value from childClassInfo.isPermittedInContainer(...)
        """
        
        #DEFAULT RULE: Selections may have Option children
        return childClassInfo.isInstanceOf('Option')
    
    #---------------------------------------------------------------#

    def onConfigureProperties(self):
        """
            Automatically called by the Knowledge Engine once
            the class has been loaded.  Use this method to register 
            class specific properties using the PropertyReflection interface.
            
            If this class is sub-classed, a call must be made to
            the super-class to ensure super-class properties are
            correctly registered
            
            NOTE: This method is called before ClassInfo.onAfterStartup()            
        """

        #initialise the super-class properties for reflection
        ComponentInfo.onConfigureProperties(self)

        # These properties are only relevant on the child "Option" objects
        # self.removePropertyDefinition('showif')
        #self.removePropertyDefinition('searchIndex')

        #add the 'displayFormat' property        
        self.addPropertyDefinition(
            PropertyDefinition(
                identity="displayFormat",
                displayName="Display As",
                description="How options are to be selected/displayed?",
                propertyType=pt_STRING,
                propertyFormat=pf_NATIVE,
                hasEnumeratedValues=true,
                enumeratedValuesList=DISPLAY_FORMAT_OPTIONS,
                defaultValue="Radio Buttons"
            )
        )

        self.addPropertyDefinition(PropertyDefinition(
            identity="nullOption", 
            displayName="Null Option", 
            defaultValue="",
            description="Enter the text to display as a null option at the top of the list",
            propertyFormat=pf_TEXT_SINGLELINE,
        ))
        
        self.addPropertyDefinition(PropertyDefinition(identity="isSorted", 
                                    displayName="Sort Options?", description="Should the options be sorted?", 
                                    propertyType=pt_BOOLEAN,
                                    propertyFormat=pf_CHECKBOX, 
                                    ))

    #---------------------------------------------------------------#

#####################################################################
## Selection Class Definition
class Selection(Component, Exportable):
    """
        The Option Component Container
    """

    #---------------------------------------------------------------#

    #TRANSIENT: a ClassInfo implementation instance to provide info about this class
    _CLASSINFO = SelectionInfo()
   
    #---------------------------------------------------------------#

    

    #---------------------------------------------------------------#

    def __init__(self, id, identity):
        "Constructor for the Selection"        

        Component.__init__(self, id, identity)
        
    #---------------------------------------------------------------#

    def isOrderedContainer(self):
        """
            Return if the children of the object may use ordering.
            
            This is to allow the 'Order' column in the Content
            management tab in Zope            
        """

        return true

    #---------------------------------------------------------------#
    
    def getIsSorted(self):
        """ Should this components vocabulary be sorted? """
        
        return getattr(self.aq_explicit, 'isSorted', False) 
    
    #---------------------------------------------------------------#
    
    def getOrderedChildren(self):
        """ Returns the list of children, sorted as specified in the component properties """
        
        if self.getIsSorted():
            
            children = self.getChildren()
            children.sort( lambda x,y: cmp(x.getContentOrTitle(), y.getContentOrTitle()))
            return children
        
        else:
            return self.getChildren()
        
        
    
    #---------------------------------------------------------------#

    def getDisplayFormat(self):
        "Return the display format of the selection"
        
        return self.displayFormat

    #---------------------------------------------------------------#

    def setDisplayFormat(self, displayFormat):
        "Set the display format of the selection"
        
        self.displayFormat = displayFormat

    #---------------------------------------------------------------#

    def getNullOption(self):
        """
            Returns the text to display as a null option at the top of the list
        """
        
        return getattr(self, 'nullOption', '')    

    #---------------------------------------------------------------#

    def setNullOption(self, value):
        """
            Sets the text to display as a null option at the top of the list
        """
        
        self.nullOption = value
        

    #---------------------------------------------------------------#

    def isMultipleSelectionPermitted(self):
        """
            Return if multiple option selection is permitted
        """
        
        return self.displayFormat == 'Check Boxes'
    
    #---------------------------------------------------------------#

    def onBeforeRenderAssessmentEditor(self, request, **args):
        """
            This method is called by Component.renderAssessmentEditor
            prior to actually rendering the assessment editor
            for this component using the Page Template (defined
            for this component).
            
            By overriding this method, individual components 
            have the ability to modify the request (add state, 
            declarations etc) prior to rendering occuring.
            
            If you override this method, a call should be 
            made to the super-class to ensure correct
            super-class initialisation.
        """

        #call the super-class handler first
        Component.onBeforeRenderAssessmentEditor(self, request, **args)
        
        #for non-dropdown formated components, we need to add the appropriate
        #javascript declarations, initialisation and refresh statements for each of the options
        if not request['ISPREVIEW'] and self.getDisplayFormat() != 'Dropdown List':            
            for option in self.getChildren():
                #add the javascript declarations and initialisation for this option
                option.addJavaScriptOnLoadStatements(request['ONLOAD_JAVASCRIPT'])
                option.addJavaScriptDeclarations(request['DECLARATIONS_JAVASCRIPT'])        

                #add the showif refresh javascript for each child as necessary
                if option.hasShowIf():
                    request['REFRESH_JAVASCRIPT'].append(option.getShowIfJavaScript(request['ASSESSMENT']))

    #---------------------------------------------------------------#

    def renderAssessmentEditor(self, request, **args):
        """
            Handles the request to render this component
            in the assessment editor.

            Actual rendering of the assessment editor is performed
            in the Component_assessmenteditor.pt

            REQUEST contains the necessary environment
            variables, established by the Model.assess call.
        """
        
        # Add showIf for the individual options
#         for child in self.getChildren():
#             if not request['ISPREVIEW'] and self.hasShowIf():
#                 request['REFRESH_JAVASCRIPT'].append(self.getShowIfJavaScript(request['ASSESSMENT']))
        
        return Component.renderAssessmentEditor(self, request, *args)
        
        
        

    #---------------------------------------------------------------#

    def generateGraphableStatistics(self, baseAssessmentFilter = None, user = None, nrAssessments = None):
        """
            Generates a Graphable instance containing statistic
            information for this component - that may be graphed.
            
            The provided assessmentFilter is used as a basis for
            generating statistics information.  Setting this to
            None (the default) means all assessments are used.
            
            RETURN TYPE: Graphable
        """
 
        #NOTE: This only applies for selections that are displayed as radio buttons or dropdown lists
        
        #ensure the baseAssessmentFilter is valid (not None)
        if baseAssessmentFilter is None:
            baseAssessmentFilter = self.getModel().getAllAssessmentsFilter()
        
        #we need to use the assessment storage service to get the statistics
        assessmentStorageService = self.getModel().getAssessmentStorageService()

        #determine the number of assessments (if necessary)
        if nrAssessments is None:
            nrAssessments = assessmentStorageService.getNrAssessmentsSatisfyingFilter(baseAssessmentFilter, user)   
        
        #we need a Graphable for the results
        graphable = Graphable(self.getTitleOrId(), "The following chart outlines the selection statistics for the '%s' component in the %d current assessments." % (self.getTitleOrId(), nrAssessments), 0, nrAssessments)
        
        #create a datum for the number of times each option has been selected in the assessments
        for option in self.getChildren():

            #create a filter for the option
            filterTitle = option.getTitleOrId()
            filterExpression = parseKERL("['%s', 'selected']" % option.getIdentity())
            assessmentFilter = AssessmentFilter(title=filterTitle, expression=simplifyKERL(['all', baseAssessmentFilter.getExpression(), filterExpression]), model=self.getModel(), assessmentScope=baseAssessmentFilter.getAssessmentScope())
            
            #filter the assessments
            nrSelectedAssessments = assessmentStorageService.getNrAssessmentsSatisfyingFilter(assessmentFilter, user)
            datum = Datum(identity=option.getIdentity(), title="# %s" % option.getTitleOrId(), value=nrSelectedAssessments, color="green", footnote="", context={"ASSESSMENT_FILTER":assessmentFilter})
            graphable.addDatum(datum)
            
            #remove the actual selections to work out the undefined count
            nrAssessments = nrAssessments - nrSelectedAssessments

        
        undefinedKerl = [self.getIdentity(), 'notattempted']

        undefinedFilter = AssessmentFilter(title="Undefined Assessments", expression=simplifyKERL(['all', baseAssessmentFilter.getExpression(), undefinedKerl]), 
                                           model=self.getModel(), assessmentScope=baseAssessmentFilter.getAssessmentScope())

        #create a datum for the number of times no options have been made in the assessments
        datum = Datum(identity="undefined", title="# Undefined.", value=nrAssessments, color="orange", context = {'ASSESSMENT_FILTER': undefinedFilter},
                      footnote="As it is possible for an end-user to avoid selecting an option, some assessments may have 'undefined' selections.")        
        graphable.addDatum(datum)
        
        return graphable

    #---------------------------------------------------------------#
    
    def compileKERLtoEnglish(self, expression):
        """ Compiles the specified KERL Statement to english """
        
        # STUB - TODO
        return ""
    
    
    #---------------------------------------------------------------#
    
    def getDependencies(self, expression):
        """
            Returns a list of components that are required to
            evaluate the component class specific KERL expression.

            NOTE: Includes the specific component itself.

            RETURN TYPE: {Component} indexed by Component.getIdentity()
       """

        return {self.getIdentity():self}

    #---------------------------------------------------------------#    

    def listActions(self, info=None):
        """
            PLONE Support:

            Returns a tuple of ActionInformation instances
            specifying the available CMF Actions for the object

            overrides:

            (CMF Actions are different from Knowledge Engine Actions)
        """

        #add the super-class actions first
        lCMFActions = Component.listActions(self, info)

        #add the 'statistics' action
        lCMFActions = lCMFActions + (
                        ActionInformation(
                            id='statistics',
                            title='statistics',
                            description='Current Statistics',
                            category='object',
                            condition='',
                            permissions=(permissions.ListFolderContents,),
                            priority=1,
                            visible=1,
                            action='string:${object_url}/Selection_Statistics'
                        ),)
                        
        return lCMFActions

    #---------------------------------------------------------------#

    def getAssessmentValueType(self):
        """
            ABSTRACT: Returns the type of the value that is to be stored
            for this component when it's assessment state is persisted.
            
            NOTE 1: See AssessmentComponentValueTypes for a list of defined contants       
            NOTE 2: This method MUST be overriden in the implementing
            class to specify the correct type for value persistence
            
            RETURN TYPE: String
        """
        
        #option assessment values are booleans
        return st_BOOLEAN
            
    #---------------------------------------------------------------#            
            
    def getDefaultAssessmentState(self, assessment):
        """
            ABSTRACT: Constructs and returns a suitable 'default' 
            AssessmentComponentState for the provided Assessment
            instance.

            NOTE: This method MUST be overriden in the implementing
            class to provide a suitable default value for the component
            
            RETURN TYPE: AssessmentComponentState
        """
        
        return AssessmentComponentState(assessment.getIdentity(), assessment.getModelIdentity(), self.getIdentity(), self.getAssessmentValueType(), None, revision=assessment.getRevision(), searchIndex=self.getSearchIndex())            

    #---------------------------------------------------------------#            

    def getComponentValue( self, assessment ):
        """ Returns this components value for the specified assessment """
        
        choices = self.getOrderedChildren()
        
        values = []
        
        for choice in choices:
            selected = choice.getComponentValue( assessment )
            if selected:
                values.append( choice.getContentOrTitle() )
            #state = assessment.getComponentState( self.getIdentity() )
            
        return values
        #return state.getValue()


    #---------------------------------------------------------------#
    # BEGIN: Exportable Implementation                              #
    #---------------------------------------------------------------#
    def getComponentCSVValue(self, assessment, displayReportContent=False):
        """
            Return the CSV friendly version of the component.
        """

        result = []
        if displayReportContent:
            for choice in self.getOrderedChildren():
                # The selection component has report content for questions
                # aren't selected. Make sure we include this
                value = choice.getComponentValue(assessment)
                choiceReport = iif(value is True, choice.getReportContentWhenSelected(), choice.getReportContentWhenUnselected())
                
                # Make sure we only add data if we have it
                if choiceReport:
                    result.append(choiceReport)
        else:
            result = self.getComponentValue(assessment)

        return ",".join(result)
    #---------------------------------------------------------------#
    # END: Exportable Implementation                                #
    #---------------------------------------------------------------#

    def hasReportContent( self ):
        """ Does this component have report content? """
        
        # Check each child option for report content
        options = self.getOrderedChildren()
        for option in options:
            if option.hasReportContent():
                return True
        
        # None of the Options have report content, so just check the Selection Question itself
        return Component.hasReportContent( self )


#####################################################################
## Class Initialisation

# Register Component Information with the Knowledge Engine
registerGenericObjectClass(Selection)

#####################################################################
